package com.erlang4j.internal.process;

import java.util.HashMap;
import java.util.Map;

import javax.swing.SwingUtilities;

import com.ericsson.otp.erlang.OtpErlangObject;
import com.erlang4j.api.Erlang4jMessageLanguage;
import com.erlang4j.api.IBinding;
import com.erlang4j.api.exceptionHandler.IExceptionHandler;
import com.erlang4j.api.exceptions.Erlang4jUnhandledMessageException;
import com.erlang4j.api.exceptions.Erlang4jWrappedException;
import com.erlang4j.api.process.IProcess;
import com.erlang4j.internal.annotations.Mutable;
import com.erlang4j.internal.basicIo.IBasicMailBox;
import com.erlang4j.internal.basicIo.MailBox;
import com.erlang4j.internal.exceptions.Erlang4jException;
import com.erlang4j.internal.messages.IMessageHandler;
import com.erlang4j.internal.stateMachine.IStateDefinition;

/**
 * <p>
 * This is the implementer of IMessageProcessor. Please see the Api for the interface for details. Please don't create
 * this directly, go through the IErlang4J interface
 * </p>
 * 
 * @author Phil Rice
 */
@Mutable("This is the implementation of a statemachine. It needs to allow the states to change the binding, and the current state")
public class StateMachine extends MailBox implements IProcess {

	private final IExceptionHandler exceptionHandler;
	private MessageProcessorThread thread;
	private final Map<String, IStateDefinition> nameToState = new HashMap<String, IStateDefinition>();

	@Mutable("This is the state that is being managed by the state machine. The message handlres need to change it")
	private IBinding matchingBinding;

	@Mutable("This is the current state that is being executed. The message handlers need to change it")
	private IStateDefinition currentState;

	public StateMachine(IBasicMailBox basicMailBox, final IExceptionHandler handler, IBinding initialBinding, IStateDefinition... stateDefinition) {
		super(basicMailBox);
		this.exceptionHandler = handler;
		this.matchingBinding = initialBinding;
		for (IStateDefinition definition : stateDefinition)
			nameToState.put(definition.getStateName(), definition);
		currentState = stateDefinition.length == 0 ? null : stateDefinition[0];
	}

	void process(OtpErlangObject object) {
		for (final IMessageHandler handler : currentState.getMessageHandler()) {
			final IBinding thisBinding = parser.parse(object, handler.pattern(), matchingBinding);
			if (thisBinding.isValid()) {
				final ProcessWrapper processWrapper = new ProcessWrapper(StateMachine.this, nameToState.keySet());
				Runnable runnable = new Runnable() {
					public void run() {
						try {
							handler.process(processWrapper, thisBinding);
						} catch (Exception e) {
							throw new Erlang4jWrappedException(e);
						}
					}
				};
				try {
					if (handler instanceof IExecuteInSwingThread)
						SwingUtilities.invokeAndWait(runnable);
					else
						runnable.run();
				} catch (Erlang4jWrappedException e) {
					if (e.getCause() instanceof RuntimeException)
						throw (RuntimeException) e.getCause();
					throw e;
				} catch (Exception e) {
					if (e.getCause() instanceof Erlang4jException)
						throw (Erlang4jException) e.getCause();
					throw new Erlang4jWrappedException(e);
				}
				matchingBinding = processWrapper.getNewMatchingBinding();
				String newStateName = processWrapper.getNewStateName();
				assert nameToState.keySet().contains(newStateName) : newStateName + " legal: " + nameToState.keySet();
				currentState = nameToState.get(newStateName);
				return;
			}
		}
		throw new Erlang4jUnhandledMessageException(object, matchingBinding);

	}

	public MessageProcessorThread spawn() {
		if (nameToState.size() == 0)
			throw new IllegalStateException("Cannot spawn if no state definitions");
		thread = new MessageProcessorThread(this, basicOperations, exceptionHandler);
		thread.setDaemon(true);
		thread.start();
		while (!thread.isAlive()) {
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}
		}
		return thread;
	}

	@Override
	public void terminate() {
		if (thread != null)
			thread.terminate();
	}

	@Override
	public boolean isAlive() {
		return thread != null && thread.isAlive();
	}

	public IBinding matchingBinding() {
		return matchingBinding;
	}

	public String getStateName() {
		return currentState == null ? "" : currentState.getStateName();
	}

	@Override
	public void bind(String key, Object object) {
		this.matchingBinding.bindAllowingChange(key, Erlang4jMessageLanguage.make(object));
	}

}
